#!/usr/bin/env python3

import asyncio
import re
import signal
import sys
import time
import slixmpp
from configparser import ConfigParser
from irc.bot import SingleServerIRCBot


class IRCBot:
    def __init__(self, opts, inter):
        self.client = SingleServerIRCBot([(opts['server'], opts['port'])],
                                           opts['nick'], opts['realname'])
        self.conn = self.client.connection
        self.nick = opts['nick']
        self.pure_nick = self.nick
        self.chan = opts['chan']
        # Support customizing the authentication bot, the commands that
        # will be sent to it, the general messages' bytes and rate limits,
        # besides supplying fallback values for most of these cases. An
        # authentication command is required with no fallback since most
        # IRC networks deny bots without registry under their service
        # database.
        self.auth_bot_name = opts['auth_bot_name']
        self.auth_bot_privmsg = opts['auth_bot_privmsg']
        self.msg_part_max_bytes = opts['msg_part_max_bytes']
        self.msg_multipart_sleep_seconds = opts['msg_multipart_sleep_seconds']
        self.inter = inter
        self.loop = None

    def register_handlers(self):
        self.conn.add_global_handler('welcome', self.on_session_start)

        # Attempt to solve the problem noted on (on_session_start). Needs
        # testing.
        self.conn.add_global_handler('disconnect', self.on_session_start)

        self.conn.add_global_handler('pubmsg', self.on_message)
        self.conn.add_global_handler('action', self.on_message)
        self.conn.add_global_handler('join', self.on_presence)
        self.conn.add_global_handler('part', self.on_presence)
        self.conn.add_global_handler('namreply', self.on_namreply)
        self.conn.add_global_handler('kick', self.on_kick)
        self.conn.add_global_handler('nicknameinuse', self.on_nicknameinuse)

    def join_chan(self):

        self.conn.part(self.chan, message='Replaced by new connection')
        self.conn.join(self.chan)

    def on_session_start(self, conn, event):
        print('Connected to IRC')
        # FIXME: Reauthenticate and rejoin channels after connection
        # loss.
        self.conn.privmsg(
            self.auth_bot_name,
            self.auth_bot_privmsg
        )
        self.join_chan()

    def on_message(self, conn, event):
        nick = event.source.split('!')[0]
        body = ''.join(event.arguments)
        typ = event.type

        if typ == 'action':
            body = '/me ' + body

        self.call_outside(self.inter.relay_message, 'irc', nick, body)

    def on_presence(self, conn, event):
        try:
            typ = event.type
            nick = event.source.nick

            if typ == 'part':
                if nick != self.nick:
                    if nick in self.inter.get_irc_users():
                        self.call_outside(self.inter.remove_irc_user, nick)

            else:
                if nick != self.nick:
                    if nick not in self.inter.get_irc_users():
                        self.call_outside(self.inter.append_irc_user, nick)
        except Exception as e:
            print(str(e), file=sys.stderr)

    def on_namreply(self, conn, event):
        for nick in event.arguments[2].split():
            if nick != conn.get_nickname():
                self.call_outside(self.inter.append_irc_user, nick)

    def on_kick(self, conn, event):
        self.nick = self.pure_nick
        conn.nick(self.nick)
        time.sleep(0.5)
        self.join_chan()

    def on_nicknameinuse(self, conn, event):
        self.nick = self.nick + '_'
        conn.nick(self.nick)

    def send_message(self, msg, lead_prefix='', prefix=''):
        # IRC standard says that maximum message size is 512 bytes,
        # including: channel/nick prefix, command, arguments, and the
        # CR+LF line ending.

        # Furthermore, tests made with the Python module reveal that
        # 460 is a good maximum size, since that module may be adding
        # other information before sending. With this in mind, the
        # possible message parts mentioned here are joined by spaces,
        # measured and subtracted from the starting limit, thus giving
        # the maximum buffer size of the message, stored in buf.

        # self.nick and self.chan are both used as prefixes since it's
        # unknown which, or if both, will be used.
        buf = min(460, self.msg_part_max_bytes) - len(' '.join(['PRIVMSG', ':' + self.nick, ':' + self.chan])) - len(b'\r\n')
        result = []

        try:
            for line in msg:
                # Since the bot itself may add lead_prefix or prefix,
                # always deduce the greatest from buf when splitting
                # lines, both when computing step for range function,
                # and when reading parts of line.
                for i in range(0, len(line), buf - max(len(prefix), len(lead_prefix))):
                    result.append(line[i:i + buf - max(len(prefix), len(lead_prefix))])

            self.conn.privmsg(self.chan, lead_prefix + result[0])

            for r in result[1:]:
                time.sleep(max(0.5, self.msg_multipart_sleep_seconds))
                self.conn.privmsg(self.chan, prefix + r)

        except Exception as e:
            print(str(e), file=sys.stderr)

    def call_outside(self, func, *args):
        assert self.loop
        self.loop.call_soon_threadsafe(func, *args)

    async def run(self):
        self.loop = asyncio.get_event_loop()
        self.register_handlers()
        await self.loop.run_in_executor(None, self.client.start)


class XMPPBot:
    def __init__(self, opts, inter, timeout=5.0):
        self.client = slixmpp.ClientXMPP(opts['jid'], opts['passwd'])

        self.client.register_plugin('xep_0199')  # XMPP Ping.
        self.client_ping = self.client.plugin['xep_0199']
        self.client.register_plugin('xep_0045')  # XMPP MUC.
        self.client_muc = self.client.plugin['xep_0045']

        self.muc_is_joined = False
        self.client_ping.timeout = self.timeout = timeout
        self.client_ping.keepalive = True

        self.nick = opts['nick']
        self.pure_nick = self.nick
        self.muc = opts['muc']
        self.inter = inter

    def register_handlers(self):
        self.client.add_event_handler('failed_all_auth',
                                      self.on_failed_all_auth)
        self.client.add_event_handler('session_start', self.on_session_start)
        self.client.add_event_handler('got_online', self.on_got_online)
        self.client.add_event_handler('disconnected', self.on_disconnected)
        self.client.add_event_handler('reconnect_delay', self.on_disconnected)
        self.client.add_event_handler('groupchat_message', self.on_message)
        self.client.add_event_handler('muc::%s::presence' % self.muc,
                                      self.on_presence)

    def connect(self):
        self.client.connect()

    def join_muc(self):
        async def loop_cycle():
            if self._join_muc_block > 0:
                return
            self._join_muc_block += 1

            while not self.muc_is_joined:
                self.client_muc.join_muc(self.muc, self.nick)
                await asyncio.sleep(self.timeout)

            self._join_muc_block -= 1

        asyncio.ensure_future(loop_cycle())
    _join_muc_block = 0

    def on_failed_all_auth(event):
        # print('could not connect!', file=sys.stderr)
        print('Could not connect to the server, or password mismatch!',
              file=sys.stderr)
        sys.exit(1)

    def on_session_start(self, event):
        # print('connected with %s' %con, file=sys.stderr)
        print('Connected to XMPP')
        self.client.get_roster()
        self.client.send_presence()

    def on_got_online(self, event):
        self.join_muc()

    async def on_disconnected(self, event):
        self.muc_is_joined = False
        print('Connection lost, reattempting in %d seconds' % self.timeout)
        await asyncio.sleep(self.timeout)
        self.connect()

    def on_message(self, event):
        body = event['body']
        nick = event['mucnick']

        self.inter.relay_message('xmpp', nick, body)

    async def on_presence(self, event):
        try:
            muc_plugin = self.client.plugin['xep_0045']
            typ = event['muc']['type']
            nick = event['muc']['nick']

            if not typ:
                typ = event['type']
            if not nick:
                nick = muc_plugin.get_nick(self.muc, event['from'])

            if typ == 'available':
                self.muc_is_joined = True
                if nick != self.nick:
                    if nick not in self.inter.get_xmpp_users():
                        self.inter.append_xmpp_user(nick)

            elif typ == 'error':
                self.muc_is_joined = False
                if event['error']['code'] == '409':
                    self.nick += '_'
                self.join_muc()

            elif typ == 'unavailable':
                if nick != self.nick:
                    if nick in self.inter.get_xmpp_users():
                        self.inter.remove_xmpp_user(nick)
                else:
                    self.muc_is_joined = False
                    self.nick = self.pure_nick
                    await asyncio.sleep(0.5)
                    self.join_muc()

            else:
                if nick != self.nick:
                    if nick not in self.inter.get_xmpp_users():
                        self.inter.append_xmpp_user(nick)

        except Exception as e:
            print(str(e), file=sys.stderr)

    def send_message(self, msg, prefix=''):
        try:
            msg[0] = prefix + msg[0]
            result = '\n'.join(msg)
            self.client.send_message(mto=self.muc, mbody=result,
                                     mtype='groupchat')

        except Exception as e:
            print(str(e), file=sys.stderr)

    async def run(self):
        self.register_handlers()
        self.connect()


class Intermedia:
    def __init__(self, shared_opts, irc_chan, xmpp_muc):
        self.irc_chan = irc_chan
        self.xmpp_muc = xmpp_muc
        self.ircbot = None
        self.xmppbot = None
        self.irc_users = []
        self.xmpp_users = []

        self.prefix = shared_opts['prefix']
        self.owner = shared_opts['owner']

    def set_bots(self, ircbot, xmppbot):
        self.ircbot = ircbot
        self.xmppbot = xmppbot

    def to_irc(self, msg, lead_prefix='', prefix=''):
        if self.ircbot:
            self.ircbot.send_message(msg, lead_prefix, prefix)

    def to_xmpp(self, msg, prefix=''):
        if self.xmppbot:
            self.xmppbot.send_message(msg, prefix)

    def relay_message(self, from_net, nick, body):
        if not self.ircbot or not self.xmppbot:
            return

        if from_net != 'irc' and from_net != 'xmpp':
            return

        if from_net == 'irc' and nick == self.ircbot.nick or \
           from_net == 'xmpp' and nick == self.xmppbot.nick:
            return

        if not body or len(body) <= 0:
            return

        try:
            msg = body.replace('\r\n', '\n').replace('\r', '\n').split('\n')

            if msg and len(msg) > 0:
                if len(msg) == 1 and msg[0] == self.prefix + 'users':
                    irc_users = ', '.join(self.get_irc_users())
                    xmpp_users = ', '.join(self.get_xmpp_users())

                    if irc_users:
                        irc_users = '[ IRC Users ] ' + irc_users
                    if xmpp_users:
                        xmpp_users = '[ XMPP Users ] ' + xmpp_users

                    if from_net == 'irc':
                        for answer in [xmpp_users]:
                            self.to_irc([answer])
                    elif from_net == 'xmpp':
                        for answer in [irc_users]:
                            self.to_xmpp([answer])

                elif len(msg) == 1 and msg[0] == self.prefix + 'help':
                    answer = 'The only command I have is \'' + self.prefix + \
                      'users\'. Also, my owner is ' + self.owner + '.'

                    if from_net == 'irc':
                        self.to_irc([answer])
                    elif from_net == 'xmpp':
                        self.to_xmpp([answer])

                else:
                    nick_prefix = '[' + nick + '] '
                    nick_prefix_me = '***' + nick + ' '

                    if (not re.match('^/me .+$', msg[0])):
                        nick_prefix_lead = nick_prefix
                    else:
                        msg[0] = re.split('^/me ', msg[0])[1]
                        nick_prefix_lead = nick_prefix_me

                    if from_net == 'irc':
                        self.to_xmpp(msg, prefix=nick_prefix_lead)
                    elif from_net == 'xmpp':
                        self.to_irc(msg,
                                    lead_prefix=nick_prefix_lead,
                                    prefix=nick_prefix)

        except Exception as e:
            print(str(e), file=sys.stderr)

    def get_irc_users(self):
        return self.irc_users

    def append_irc_user(self, user):
        self.irc_users.append(user)

    def remove_irc_user(self, user):
        self.irc_users.remove(user)

    def get_xmpp_users(self):
        return self.xmpp_users

    def append_xmpp_user(self, user):
        self.xmpp_users.append(user)

    def remove_xmpp_user(self, user):
        self.xmpp_users.remove(user)

if __name__ == '__main__':
    config = ConfigParser()
    shared_opts = {}
    xmpp_opts = {}
    irc_opts = {}

    if len(sys.argv) > 1:
        config.read(sys.argv[1])
    else:
        config.read('config.ini')

    if not config.sections():
        print('Error: Configuration file does not exist or is empty.',
              file=sys.stderr)
        sys.exit(1)

    shared_opts['prefix'] = config.get('Shared', 'prefix')
    shared_opts['owner'] = config.get('Shared', 'owner')

    irc_opts['chan'] = config.get('IRC', 'channel')
    irc_opts['nick'] = config.get('IRC', 'nick')
    # Support realname, a short string that can have spaces and describes
    # the bot to others on channel or when WHOIS/WHOWAS is used.
    irc_opts['realname'] = config.get('IRC', 'realname', fallback = irc_opts['nick'])
    irc_opts['server'] = config.get('IRC', 'server')
    irc_opts['port'] = int(config.get('IRC', 'port'))
    # Support customizing the authentication bot, the commands that will
    # be sent to it, the general messages' bytes and rate limits,
    # besides supplying fallback values for most of these cases. An
    # authentication command is required with no fallback since most IRC
    # networks deny bots without registry under their service database.
    irc_opts['auth_bot_name'] = config.get('IRC', 'auth_bot_name', fallback = 'NickServ')
    irc_opts['auth_bot_privmsg'] = config.get('IRC', 'auth_bot_privmsg')
    irc_opts['msg_part_max_bytes'] = int(config.get('IRC', 'msg_part_max_bytes', fallback = 460))
    irc_opts['msg_multipart_sleep_seconds'] = float(config.get('IRC', 'msg_multipart_sleep_seconds', fallback = 0.5))

    xmpp_opts['jid'] = config.get('XMPP', 'jid')
    xmpp_opts['passwd'] = config.get('XMPP', 'password')
    xmpp_opts['muc'] = config.get('XMPP', 'muc')
    xmpp_opts['nick'] = config.get('XMPP', 'nick')

    signal.signal(signal.SIGINT, signal.SIG_DFL)
    loop = asyncio.get_event_loop()

    inter = Intermedia(shared_opts, irc_opts['chan'], xmpp_opts['muc'])
    ircbot = IRCBot(irc_opts, inter)
    xmppbot = XMPPBot(xmpp_opts, inter)
    inter.set_bots(ircbot, xmppbot)

    asyncio.ensure_future(xmppbot.run())
    asyncio.ensure_future(ircbot.run())
    loop.run_forever()
